#include <cassert>


#include "ScriptDocumentOperation.h"

#include "simodo/module/ModuleManagement.h"
#include "simodo/lsp-client/SimodoCommandResult.h"
#include "simodo/inout/reporter/StreamReporter.h"

#if __cplusplus >= __cpp_2017
#include <filesystem>
namespace fs = std::filesystem;
#else
#include <experimental/filesystem>
namespace fs = std::filesystem::experimental;
#endif

using namespace simodo;

ScriptDocumentOperation::ScriptDocumentOperation(lsp::DocumentContext &doc, 
                                                 const DocumentOperationFactory & factory,
                                                 std::string languageId)
    : _doc(doc) 
    , _factory(factory)
    , _languageId(std::move(languageId))
{
}

bool ScriptDocumentOperation::checkDependency(const std::string & uri) const
{
    const auto & contains = [&uri](const std::string & file) { return uri == file; };

    return std::ranges::any_of(_files, contains);
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////

bool ScriptDocumentOperation::isRemoteFunctionLaunch(const inout::TokenLocation & using_location) const {
    const auto & IsRangeEqual = [](const inout::TokenLocation & target_location)
    {
        return [&target_location] (const inout::TokenLocation & given_location) { return given_location.range() == target_location.range(); };
    };

    return std::ranges::any_of(_semantic_data.remotes(), IsRangeEqual(using_location));
}


lsp::Range ScriptDocumentOperation::getFunctionBodyRange(const variable::Variable & var) {
    assert(var.type() == variable::ValueType::Function);

    const auto   function_object     = var.value().getObject();
    const auto & function_parameters = function_object->variables();

    assert(function_parameters[0].type() == variable::ValueType::IntFunction);
    const auto &[code, closures, parent] = function_parameters[0].value().getIntFunction();

    assert(!code->branches().empty());

    assert(int(code->operation()) == 3107);
    // assert(int(code->branches().back().operation()) == 0);

    const auto & openBracketToken  = code->token();
    const auto & closeBracketToken = code->branches().back().token();

    assert(openBracketToken.location().range().start() <= closeBracketToken.location().range().end());

    return { openBracketToken.location().range().start(), closeBracketToken.location().range().end() };
}


/// Шаблонная функция превращения некоторых предметов через заданную функцию и разделитель в строку
template<typename T>
static std::u16string join(const typename std::vector<T>::const_iterator begin,
                           const typename std::vector<T>::const_iterator end,
                           std::function<std::u16string(const T &)> convert,
                           const std::u16string & delimiter = u", ") {
    std::u16string text;

    for (auto it = begin; it != end; ++it) {
        if (it != begin) text += delimiter;

        text += convert(*it);
    }

    return text;
}


std::u16string getValueTypeName(const variable::ValueType type) {
    switch (type) {
        case variable::ValueType::Null:
            return u"void";
        default:
            return inout::toU16(variable::getValueTypeName(type));
    }
}

inline std::u16string getVariableName(const variable::Variable & var) { return var.name(); }

/// Базовое преобразование переменной в текст, подобный исходному коду, содержащий тип и имя.
/// Например, "int : x".
inline std::u16string getVariableBaseString(const variable::Variable & var) {
    return ::getValueTypeName(var.type()) + u" : " + getVariableName(var);
}

/// Преобразование переменной типа ValueType::Function в текст, подобный исходному коду.
/// Например, "fn func(int : no) -> bool".
/// \attention Поддерживается только для типа ValueType::Function!
std::u16string getFunctionString(const variable::Variable & var) {
    assert(var.type() == variable::ValueType::Function);

    const auto   function_object             = var.value().getObject();
    const auto & function_parameters         = function_object->variables();

    const auto & function_return_type        = function_parameters[1].value().type();
    const auto & function_return_type_string = ::getValueTypeName(function_return_type);

    const auto & params_string = join<variable::Variable>(function_parameters.begin() + 2, function_parameters.end(), getVariableBaseString);

    assert(function_parameters[0].type() == variable::ValueType::IntFunction
        || function_parameters[0].type() == variable::ValueType::ExtFunction);

    std::u16string closures_string;
    if (function_parameters[0].type() == variable::ValueType::IntFunction) {
        const auto & [code, closures, parent] = function_parameters[0].value().getIntFunction();

        if (const auto & closures_parameters = closures.variables(); not closures_parameters.empty()) {
            // \todo Сделать проверку на замыкания типа "[*]", check/031-14-FunctionDefinition

            const auto & closures_parameters_string =
                    join<variable::Variable>(closures_parameters.begin(), closures_parameters.end(), getVariableName);

            closures_string = u" [" + closures_parameters_string + u"]";
        }
    }

    return u"fn " + var.name() + closures_string + u" (" + params_string + u")" + u" -> " + function_return_type_string;

}

bool isPrimitiveType(const variable::ValueType type) {
    // \todo Полагаю, стоит эту проверку вынести куда-то ещё
    return    type == variable::ValueType::Bool
           or type == variable::ValueType::Int
           or type == variable::ValueType::Null
           or type == variable::ValueType::Float
           or type == variable::ValueType::String;
}

std::u16string ScriptDocumentOperation::getVariableString(const variable::Variable & var) {
    std::u16string text;

    if (var.type() == variable::ValueType::Function) {
        text = getFunctionString(var);
    } else {
        text = getVariableBaseString(var);
        if (isPrimitiveType(var.type()))
            text += u" = " + variable::toString(var.value());
    }

    return text;
}
